%% @copyright 2021-2024 Marc Worrell
%% @author Marc Worrell <marc@worrell.nl>
%% @doc Add functionality to the admin to edit images.
%% @end

%% Copyright 2021-2024 Marc Worrell
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%%     http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.

-module(mod_image_edit).
-moduledoc("
See also

[image](/id/doc_template_tag_tag_image), [Arguments](/id/doc_template_tag_tag_image)

Non destructive edits of images.

This adds a *Edit image* button on the media panel in the admin.

Clicking that button opens an image editor where the following editing parameters can be set:

*   Rotation: in 90˚ increments, used to correct the orientation of an image
*   Crop: crop any or all sides of an image
*   Brightness: make an image more or less bright
*   Contrast: enhances or lowers the contrast
*   Roll: correct the rotation of an image (between 45˚ and -45˚)
*   Tilt: rotate an image in the Y direction, as if the viewer moves up or down
*   Pan: rotate an image in the X direction, as if the viewer moves left or right
*   Lossless flag: use PNG or GIF for the output image, used for clip art and logos
*   Crop center: mark a point that should stay in view when using automatic crops

All these operations are non-destructive. They are stored in the `medium_edit_settings` properties and applied whenever
an image is resized using the `{% image %}` tag.

The original image stays untouched. Downloads will download the original image.

If the original image should be used for a preview then pass the `original` argument to the `{% image %}` tag options.
").
-author("Marc Worrell <marc@worrell.nl>").

-mod_title("Image Edit").
-mod_description("Edit uploaded images.").
-mod_prio(500).

-include_lib("zotonic_core/include/zotonic.hrl").

-export([
    event/2,

    observe_media_upload_rsc_props/3,
    observe_rsc_update/3,
    observe_media_preview_options/3
]).

event(#submit{ message = {edit_form, Args} }, Context) ->
    {id, ArgId} = proplists:lookup(id, Args),
    OnSuccess = proplists:get_all_values(on_success, Args),
    Id = m_rsc:rid(ArgId, Context),
    case z_acl:rsc_editable(Id, Context) of
        true ->
            Settings = z_context:get_q_all_noz(Context),
            case m_image_edit:save_settings(Id, Settings, Context) of
                {ok, _} ->
                    Context1 = z_render:growl(?__("Saved the image settings.", Context), Context),
                    z_render:wire(OnSuccess, Context1);
                {error, _} ->
                    z_render:growl_error(?__("Could not save the image settings.", Context), Context)
            end;
        false ->
            z_render:growl_error(?__("You don't have permission to edit this page.", Context), Context)
    end.


observe_media_upload_rsc_props(#media_upload_rsc_props{ options = Options }, Props, _Context) ->
    case maps:get(<<"medium_edit_settings">>, Props, undefined) of
        M when is_map(M) ->
            Props;
        _ ->
            case z_convert:to_bool(proplists:get_value(is_import, Options)) of
                true ->
                    Props;
                false ->
                    Props#{ <<"medium_edit_settings">> => #{} }
            end
    end.

observe_rsc_update(#rsc_update{}, {ok, #{ <<"medium_edit_settings">> := undefined }} = Acc, _Context) ->
    Acc;
observe_rsc_update(#rsc_update{}, {ok, #{ <<"medium_edit_settings">> := Settings } = Props}, _Context) when is_map(Settings) ->
    Sanitized = m_image_edit:sanitize(Settings),
    {ok, Props#{
        <<"medium_edit_settings">> => Sanitized
    }};
observe_rsc_update(#rsc_update{}, {ok, #{ <<"medium_edit_settings">> := _ } = Props}, _Context) ->
    {ok, Props#{
        <<"medium_edit_settings">> => undefined
    }};
observe_rsc_update(_, Acc, _Context) ->
    Acc.


%% @doc Expand the image modifications that influence the generated preview image.
%% The order of the operations is important. As they are added in front of the
%% options list the last added option will be performed first.
%% The order of the operations is:
%%
%% <ol>
%% <li>Lossless (just an option for later image compression)</li>
%% <li>Angle</li>
%% <li>Tilt</li>
%% <li>Pan</li>
%% <li>Rotate  (90deg increments)</li>
%% <li>Crop</li>
%% <li>Contrast</li>
%% <li>Brightness</li>
%% </ol>
%%
observe_media_preview_options(#media_preview_options{ id = undefined }, Options, _Context) ->
    Options;
observe_media_preview_options(#media_preview_options{ id = Id, width = W, height = H }, Options, Context) ->
    {ok, Map} = m_image_edit:get_settings(Id, Context),
    expand_edit_settings(Map, W, H, Options, Context).


expand_edit_settings(Settings, W, H, Options, Context) ->
    % Contrast & brightness are last
    OptionsB = case maps:get(<<"brightness">>, Settings, 0) of
        0 -> Options;
        Brightness -> [ {brightness, Brightness} | Options ]
    end,
    OptionsCB = case maps:get(<<"contrast">>, Settings, 0) of
        0 -> OptionsB;
        Contrast -> [ {contrast, Contrast} | OptionsB ]
    end,

    % Crop before contrast & brightness
    CL = maps:get(<<"crop_left">>, Settings, 0.0),
    CR = maps:get(<<"crop_right">>, Settings, 0.0),
    CT = maps:get(<<"crop_top">>, Settings, 0.0),
    CB = maps:get(<<"crop_bottom">>, Settings, 0.0),

    OptionsCrop = case nz(CL) orelse nz(CR) orelse nz(CT) orelse nz(CB) of
        true ->
            [ {cropp, [ CL, CR, CT, CB]}  | OptionsCB ];
        false ->
            OptionsCB
    end,

    % Rotate before crop
    Options6 = case maps:get(<<"rotate">>, Settings, 0) of
        0 -> OptionsCrop;
        R -> [ {rotate, R} | OptionsCrop ]
    end,

    % Tilt, angle and pan before rotate
    Roll = maps:get(<<"roll">>, Settings, 0),
    Tilt = maps:get(<<"tilt">>, Settings, 0),
    Pan = maps:get(<<"pan">>, Settings, 0),
    Options7 = case {Roll, Tilt, Pan} of
        {0, 0, 0} -> Options6;
        _ -> [{rotate3d, [ Roll, Tilt, Pan ]} | Options6 ]
    end,

    % Lossless as first
    OptionsLL = case maps:get(<<"is_lossless">>, Settings, false) of
        true ->
            [ {lossless, true} | Options7 ];
        false ->
            Options7
    end,

    % Crop center - defined in fractions - recalculated to pixels on the original image.
    % The image preview routines will map the crop coordinate using cropp and rotation.
    CropX = maps:get(<<"crop_center_x">>, Settings, -1),
    CropY = maps:get(<<"crop_center_y">>, Settings, -1),
    OptionsCropCenter = case CropX >= 0.0 andalso CropY >= 0.0 of
        true ->
            CropXPix = trunc(CropX * W),
            CropYPix = trunc(CropY * H),
            opt_crop_center([ CropXPix, CropYPix ], OptionsLL, Context);
        false ->
            OptionsLL
    end,
    OptionsCropCenter.

% Test if a floating point value is non-zero
nz(V) -> V >= 0.1 orelse V =< -0.1.

opt_crop_center(CropXY, Options, Context) ->
    Crop = proplists:get_value(crop, Options),
    case is_crop_center(Crop) of
        true ->
            add_crop_center(CropXY, Options);
        false when Crop =:= undefined ->
            opt_crop_center_mediaclass(proplists:get_value(mediaclass, Options), CropXY, Options, Context);
        false ->
            Options
    end.

opt_crop_center_mediaclass(undefined, _CropXY, Options, _Context) ->
    Options;
opt_crop_center_mediaclass(Mediaclass, CropXY, Options, Context) ->
    {ok, Props, _Hash} = z_mediaclass:get(Mediaclass, Context),
    case is_crop_center(proplists:get_value(crop, Props)) of
        true -> add_crop_center(CropXY, Options);
        false -> Options
    end.

add_crop_center(CropXY, Options) ->
    z_utils:prop_replace(crop, CropXY, Options).

is_crop_center(true) -> true;
is_crop_center(center) -> true;
is_crop_center(<<"center">>) -> true;
is_crop_center("center") -> true;
is_crop_center(_) -> false.

